<?php

/*
Copyright 2005-2012 Textpattern CMS Project
All Rights Reserved.

This file is part of Textpattern.

Textpattern is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

if (!defined('txp'))
{
	header('HTTP/1.1 403 Forbidden');
	exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
}

/** 
 * Controller for all subtabs of the Presentation tab.
 *
 */
class _PresentationController extends TXPAdminController
{
	private $newPageName;
	private $defaultPageName;
	private $newStyleName;
	private $defaultStyleName;

	/** 
	 * @var input GET / POST / COOKIE input variables 
	 */
	protected $input;

	
	//---------------------------------------------------------------------------
	// Public Methods
	//---------------------------------------------------------------------------

	public function __construct($app)
	{
		parent::__construct($app);
		$this->input = $this->factory->manufacture('Input', (array)$_GET, (array)$_POST, (array)$_COOKIE);
	}

	//---------------------------------------------------------------------------
	/**
	 * Redirect to the default action when visiting the main Presentation screen.
	 */
	public function action_index($params)
	{
		$this->getCommonVars($vars);
		$this->redirect($vars['urls'][$vars['tab_default']['presentation']]);
	}

	//---------------------------------------------------------------------------
	/**
	 * Public jump-off point for handling the Sections subscreen.
	 * 
	 * @param array params State information from the current URL
	 */
	public function action_sections($params)
	{
		$this->getCommonVars($vars);
		$vars['selected_subtab'] = 'sections';
		$vars['body_css_id'] = 'page-section';

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Public jump-off point for handling the Pages subscreen.
	 * 
	 * @param array params State information from the current URL
	 */
	public function action_pages($params)
	{
		$model = $this->newModel('Pages');
		$this->defaultPageName = $model->defaultPage();

		switch (@$params[0])
		{
			case 'new':
				return $this->pages_new($this->dropParam($params));
			case '':
				$params[0] = $this->defaultPageName;
				return $this->pages_edit($params);
			case 'edit':
				return $this->pages_edit($this->dropParam($params));
			case 'delete':
				return $this->pages_delete($this->dropParam($params));
		}
	
		throw new SparkHTTPException_NotFound("action not found: {$params[0]}");
	}

	//---------------------------------------------------------------------------
	/**
	 * Public jump-off point for handling the Forms (Partials?) subscreen
	 * 
	 * @param array params State information from the current URL
	 */
	public function action_forms($params)
	{
		$this->getCommonVars($vars);
		$vars['selected_subtab'] = 'forms';
		$vars['body_css_id'] = 'page-form';

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Public jump-off point for handling the Styles subscreen.
	 * 
	 * @param array params State information from the current URL
	 */
	public function action_styles($params)
	{
		$model = $this->newModel('Styles');
		$this->defaultStyleName = $model->defaultStyle();

		switch (@$params[0])
		{
			case 'new':
				return $this->styles_new($this->dropParam($params));
			case '':
				$params[0] = $this->defaultStyleName;
				return $this->styles_edit($params);
			case 'edit':
				return $this->styles_edit($this->dropParam($params));
			case 'delete':
				return $this->styles_delete($this->dropParam($params));
		}
	
		throw new SparkHTTPException_NotFound("action not found: {$params[0]}");
	}


	//---------------------------------------------------------------------------
	// Protected Methods
	//---------------------------------------------------------------------------

	/**
	 * Form for creating a new Page.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function pages_new($params)
	{
		$this->getCommonVars($vars);
		self::$lang->load('page');

		$form = $this->getPageForm(true);

		$form->input($params['pv']);

		$model = $this->newModel('Pages');
		if ($form->run(array('model'=>$model)) === true)
		{
			$this->session->flashSet('notice', self::$lang->get('page_created', $this->newPageName));
			$this->redirect($vars['urls']['pages'].'/edit/' . $this->newPageName);
		}

		$vars['selected_subtab'] = 'pages';
		$vars['body_css_id'] = 'page-page';
		$vars['form'] = $form;
		$vars['pages'] = $this->pages_list($model);
		$vars['links']['new_page'] =  $this->urlTo($vars['urls']['pages'].'/new');
		$vars['lang'] = self::$lang;

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Form for editing / cloning an existing Page.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function pages_edit($params)
	{
		if (!$pageName = @$params[0])
		{
			throw new SparkHTTPException_NotFound('no page name');
		}

		$this->getCommonVars($vars);
		self::$lang->load('page');

		// If form was POSTed, don't call findPageByName() - that's just a wasted call to the database.
		// Instead, build a page object based on submitted fields.

		// Sanity-check the name supplied in the URL against the hidden name field.
		$model = $this->newModel('Pages');
		$form = $this->getPageForm(false, $pageName);

		if (empty($params['pv']))
		{
			$page = $model->findPageByName($pageName);
			if (!$page)
			{
				throw new SparkHTTPException_NotFound('page not found');
			}
			$form->defaults
			(
				array
				(
					'user_html' => $page['user_html'],
				)
			);
		}
		else
		{
			if ($pageName != $params['pv']['name'])
			{
				throw new SparkHTTPException_NotFound('invalid page name');
			}
			$form->input($params['pv']);
		}

		$pageCopied = false;
		if ($form->run(array('model'=>$model, 'name'=>$pageName, 'copied'=>&$pageCopied)) === true)
		{
			$form->reset();	// generate new nonce
			$notice = self::$lang->get($pageCopied ? 'page_created' : 'page_updated', $pageCopied ? $params['pv']['copyName'] : $pageName);
			$vars['notice'] = $notice;
			if ($pageCopied)
			{
				$this->session->flashSet('notice', $notice);
				$this->redirect($vars['urls']['pages'].'/edit/' . $params['pv']['copyName']);
			}
		}
		elseif (!$form->submitted())
		{
			$vars['notice'] = $this->session->flashGet('notice');
		}

		// Populate the state information, ready to render
		$vars['selected_subtab'] = 'pages';
		$vars['body_css_id'] = 'page-page';
		$vars['form'] = $form;
		$vars['pages'] = $this->pages_list($model, $pageName);
		$vars['links']['new_page'] =  $this->urlTo($vars['urls']['pages'].'/new');
		$vars['lang'] = self::$lang;

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Generate a list of defined Pages.
	 * 
	 * @param SparkModel model The pages model
	 * @param string pageName Name of current Page being edited
	 * @return array Pages collection
	 */
	protected function pages_list($model, $pageName=NULL)
	{
		$this->getCommonVars($vars);
		$pages = array();

		foreach ($model->findPages(NULL, 'name') as $info)
		{
			unset($page);
			$page['name'] = $info['name'];
			$page['link'] = $this->urlTo($vars['urls']['pages'].'/edit/'.$page['name']);
			$page['default'] = ($info['name'] == $this->defaultPageName);

			if (strcasecmp($pageName, $page['name']) === 0)
			{
				$page['selected'] = true;
			}

			$page['delete_form'] = $this->getDeletePageForm($page['name']);
			$page['delete_form']->run();
			$pages[] = $page;
		}

		return $pages;
	}

	//---------------------------------------------------------------------------
	/**
	 * Delete a Page.
	 * 
	 * This method is only invoked via POST.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function pages_delete($params)
	{
		$this->getCommonVars($vars);
		self::$lang->load('page');

		if (empty($params['pv']['name']))
		{
			throw new SparkHTTPException_BadRequest();
		}

		if ((!$pageName = @$params[0]) || (strcasecmp($pageName, @$params['pv']['name']) !== 0))
		{
			throw new SparkHTTPException_NotFound('invalid page name');
		}

		$model = $this->newModel('Pages');
		$model->deletePage(array('name'=>$pageName));
		$this->session->flashSet('notice', self::$lang->get('page_deleted', $pageName));

		$this->redirect($vars['urls']['pages']);
	}

	//---------------------------------------------------------------------------
	/**
	 * Form for creating a new stylesheet.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function styles_new($params)
	{
		$this->getCommonVars($vars);
		self::$lang->load('style');

		$form = $this->getStyleForm(true);

		$form->input($params['pv']);
		
		$model = $this->newModel('Styles');
		if ($form->run(array('model'=>$model)) === true)
		{
			$this->session->flashSet('notice', self::$lang->get('css_created', $this->newStyleName));
			$this->redirect($vars['urls']['styles'].'/edit/' . $this->newStyleName);
		}

		$vars['selected_subtab'] = 'styles';
		$vars['body_css_id'] = 'page-css';
		$vars['form'] = $form;
		$vars['styles'] = $this->styles_list($model);
		$vars['links']['new_style'] =  $this->urlTo($vars['urls']['styles'].'/new');
		$vars['lang'] = self::$lang;

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Form for editing / cloning an existing stylesheet.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function styles_edit($params)
	{
		if (!$styleName = @$params[0])
		{
			throw new SparkHTTPException_NotFound('no style name');
		}

		$this->getCommonVars($vars);
		self::$lang->load('style');

		// If form was POSTed, don't call findStyleByName() - that's just a wasted call to the database.
		// Instead, build a style object based on submitted fields.

		// Sanity-check the name supplied in the URL against the hidden name field.
		$model = $this->newModel('Styles');
		$form = $this->getStyleForm(false, $styleName);

		if (empty($params['pv']))
		{
			$style = $model->findStyleByName($styleName);
			if (!$style)
			{
				throw new SparkHTTPException_NotFound('style not found');
			}
			$form->defaults
			(
				array
				(
					'css' => $style['css'],
				)
			);
		}
		else
		{
			if ($styleName != $params['pv']['name'])
			{
				throw new SparkHTTPException_NotFound('invalid style name');
			}
			$form->input($params['pv']);
		}

		$styleCopied = false;
		if ($form->run(array('model'=>$model, 'name'=>$styleName, 'copied'=>&$styleCopied)) === true)
		{
			$form->reset();	// generate new nonce
			$notice = self::$lang->get($styleCopied ? 'css_created' : 'css_updated', $styleCopied ? $params['pv']['copyName'] : $styleName);
			$vars['notice'] = $notice;
			if ($styleCopied)
			{
				$this->session->flashSet('notice', $notice);
				$this->redirect($vars['urls']['styles'].'/edit/' . $params['pv']['copyName']);
			}
		}
		elseif (!$form->submitted())
		{
			$vars['notice'] = $this->session->flashGet('notice');
		}

		// Populate the state information, ready to render
		$vars['selected_subtab'] = 'styles';
		$vars['body_css_id'] = 'page-css';
		$vars['form'] = $form;
		$vars['styles'] = $this->styles_list($model, $styleName);
		$vars['links']['new_style'] =  $this->urlTo($vars['urls']['styles'].'/new');
		$vars['lang'] = self::$lang;

		$this->render('default', $vars);
	}

	//---------------------------------------------------------------------------
	/**
	 * Generate a list of defined stylesheets.
	 * 
	 * @param SparkModel model The styles model
	 * @param string styleName Name of current style being edited
	 * @return array Stylesheet collection
	 */
	protected function styles_list($model, $styleName=NULL)
	{
		$this->getCommonVars($vars);
		$styles = array();

		foreach ($model->findStyles(NULL, 'name') as $info)
		{
			unset($style);
			$style['name'] = $info['name'];
			$style['link'] = $this->urlTo($vars['urls']['styles'].'/edit/'.$style['name']);
			$style['default'] = ($info['name'] == $this->defaultStyleName);

			if (strcasecmp($styleName, $style['name']) === 0)
			{
				$style['selected'] = true;
			}

			$style['delete_form'] = $this->getDeleteStyleForm($style['name']);
			$style['delete_form']->run();
			$styles[] = $style;
		}

		return $styles;
	}

	//---------------------------------------------------------------------------
	/**
	 * Delete a stylesheet.
	 * 
	 * This method is only invoked via POST.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function styles_delete($params)
	{
		$this->getCommonVars($vars);
		self::$lang->load('style');

		if (empty($params['pv']['name']))
		{
			throw new SparkHTTPException_BadRequest();
		}

		if ((!$styleName = @$params[0]) || (strcasecmp($styleName, @$params['pv']['name']) !== 0))
		{
			throw new SparkHTTPException_NotFound('invalid style name');
		}

		$model = $this->newModel('Styles');
		$model->deleteStyle(array('name'=>$styleName));
		$this->session->flashSet('notice', self::$lang->get('css_deleted', $styleName));

		$this->redirect($vars['urls']['styles']);
	}

	//---------------------------------------------------------------------------
	/**
	 * Populate the environment variables.
	 * 
	 * @param array params Environment and state information from the URL
	 */
	protected function getCommonVars(&$vars)
	{
		parent::getCommonVars($vars);
		$vars['selected_tab'] = 'presentation';
		$vars['body_css_class'] = 'presentation';
	}

	//---------------------------------------------------------------------------
	/**
	 * Inline sanitization of a page name.
	 * 
	 * @param string text The name of the page to sanitize
	 * @param array errors Error information placeholder
	 * @return string valid, sanitized page name
	 */
	public function validate_page_name($text, &$errors) {
		$errors = array();
		return $this->input->validate_for_url($text);
	}

	//---------------------------------------------------------------------------
	/**
	 * Inline sanitization of a style name.
	 * 
	 * @param string text The name of the stylesheet to sanitize
	 * @param array errors Error information placeholder
	 * @return string valid, sanitized stylesheet name
	 */
	public function validate_style_name($text, &$errors) {
		$errors = array();
		return $this->input->validate_for_url($text);
	}


	//---------------------------------------------------------------------------
	// Callback Methods
	//---------------------------------------------------------------------------

	/**
	 * Form-based validation for new pages.
	 * 
	 * Any failure prevents the form being submitted.
	 *
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the validation checks passed
	 */
	public function _validateForm_newPage($input, $params, $errObj)
	{
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Create the given page.
	 * 
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the processing completed successfully
	 */
	public function _processForm_newPage($input, $params, $errObj)
	{
		$model = $params['model'];
		$page = $model->newPage($input);
		$model->addPage($page);
		$this->newPageName = $page['name'];
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Form-based validation for existing pages.
	 * 
	 * Any failure prevents the form being submitted.
	 *
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the validation checks passed
	 */
	public function _validateForm_editPage($input, $params, $errObj)
	{
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Update or clone the given page.
	 * 
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the processing completed successfully
	 */
	public function _processForm_editPage($input, $params, $errObj)
	{
		$model = $params['model'];

		if (isset($input['save']))
		{
			$model->updatePage($input);
		}
		else
		{
			$input['name'] = $input['copyName'];
			$model->addPage($input);
			$params['copied'] = true;
		}
		
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Form-based validation for new styles.
	 * 
	 * Any failure prevents the form being submitted.
	 *
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the validation checks passed
	 */
	public function _validateForm_newStyle($input, $params, $errObj)
	{
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Create the given stylesheet.
	 * 
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the processing completed successfully
	 */
	public function _processForm_newStyle($input, $params, $errObj)
	{
		$model = $params['model'];
		$style = $model->newStyle($input);
		$model->addStyle($style);
		$this->newStyleName = $style['name'];
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Form-based validation for existing styles.
	 * 
	 * Any failure prevents the form being submitted.
	 *
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the validation checks passed
	 */
	public function _validateForm_editStyle($input, $params, $errObj)
	{
		return true;
	}

	//---------------------------------------------------------------------------
	/**
	 * Update or clone the given stylesheet.
	 * 
	 * @param array input User-submitted form data
	 * @param array params Model data block
	 * @param array errObj Error placeholder
	 * @return bool Whether the processing completed successfully
	 */
	public function _processForm_editStyle($input, $params, $errObj)
	{
		$model = $params['model'];

		if (isset($input['save']))
		{
			$model->updateStyle($input);
		}
		else
		{
			$input['name'] = $input['copyName'];
			$model->addStyle($input);
			$params['copied'] = true;
		}
		
		return true;
	}

	//---------------------------------------------------------------------------
	// Private Methods
	//---------------------------------------------------------------------------

	/**
	 * Generate the HTML form for managing page content
	 * 
	 * @param bool isNew Whether the action is to create a new page
	 * @param string pageName The name of the page being edited
    * @return SparkForm HTML elements for the main input controls
	 */
	private function getPageForm($isNew, $pageName = NULL)
	{

		$form = $this->newForm($isNew ? 'page_new' : 'page_edit', $isNew ? '' : self::$lang->get('you_are_editing_page', $pageName) )
			->validationCallback(array($this, $isNew ? '_validateForm_newPage' : '_validateForm_editPage'))
			->processingCallback(array($this, $isNew ? '_processForm_newPage' : '_processForm_editPage'))
			->validator($this)
				;

		if ($isNew)
		{
			$form->text('name', self::$lang->get('name_for_this_page'), NULL, NULL, true, 'page_name');
		}
		
		$form
			->textArea('user_html', '', NULL, array('class'=>'code large'))
			->submit('save', NULL, self::$lang->get('save'), array('class' => 'button-primary'), true)
				;
		
		if (!$isNew)
		{
			$form
				->hidden('name', $pageName)
				->fieldset_open('copy_page_as')
				->text('copyName', self::$lang->get('copy_page_as'), NULL, NULL, false, 'required_if[copy]')
				->submit('copy', '', self::$lang->get('copy'), array('class' => 'button-secondary'), true)
				->fieldset_close()
					;
		}

		return $form;
	}

	//---------------------------------------------------------------------------
	/**
	 * Generate the HTML form for handling page deletion
	 * 
	 * @param string pageName The name of the page upon which to add the associated delete button
    * @return SparkForm HTML elements for the delete input controls
    * @todo 'Are you sure?' action confirmation message
	 */
	private function getDeletePageForm($pageName)
	{
		$this->getCommonVars($vars);
		$form = $this->newForm('page_delete_'.$pageName, 'Delete Page')
			->honeypot(false)
			->action($this->urlTo($vars['urls']['pages'].'/delete/'.$pageName))
			->submit('delete', NULL, self::$lang->get('btn_delete'), array('id'=>'delete_'.$pageName, 'class'=>'smallerbox'), true)
			->hidden('name', $pageName, array('id'=>'delete_name_'.$pageName))
			->setFormClass('delete')
				;
		
		return $form;
	}

	//---------------------------------------------------------------------------
	/**
	 * Generate the HTML form for the tag builder
	 * 
	 */
	private function getTagBuilderPageForm()
	{
		return true;
	}

	/**
	 * Generate the HTML form for managing stylesheet content
	 * 
	 * @param bool isNew Whether the action is to create a new stylesheet
	 * @param string styleName The name of the stylesheet being edited
    * @return SparkForm HTML elements for the main input controls
	 */
	private function getStyleForm($isNew, $styleName = NULL)
	{

		$form = $this->newForm($isNew ? 'style_new' : 'style_edit', $isNew ? '' : self::$lang->get('you_are_editing_css', $styleName) )
			->validationCallback(array($this, $isNew ? '_validateForm_newStyle' : '_validateForm_editStyle'))
			->processingCallback(array($this, $isNew ? '_processForm_newStyle' : '_processForm_editStyle'))
			->validator($this)
				;

		if ($isNew)
		{
			$form->text('name', self::$lang->get('name_for_this_style'), NULL, NULL, true, 'style_name');
		}
		
		$form
			->textArea('css', '', NULL, array('class'=>'code large'))
			->submit('save', NULL, self::$lang->get('save'), array('class' => 'button-primary'), true)
				;
		
		if (!$isNew)
		{
			$form
				->hidden('name', $styleName)
				->fieldset_open('copy_css_as')
				->text('copyName', self::$lang->get('copy_css_as'), NULL, NULL, false, 'required_if[copy]')
				->submit('copy', '', self::$lang->get('copy'), array('class' => 'button-secondary'), true)
				->fieldset_close()
					;
		}

		return $form;
	}

	//---------------------------------------------------------------------------
	/**
	 * Generate the HTML form for handling stylesheet deletion
	 * 
	 * @param string styleName The name of the stylesheet upon which to add the associated delete button
    * @return SparkForm HTML elements for the delete input controls
    * @todo 'Are you sure?' action confirmation message
	 */
	private function getDeleteStyleForm($styleName)
	{
		$this->getCommonVars($vars);
		$form = $this->newForm('style_delete_'.$styleName, 'Delete Style')
			->honeypot(false)
			->action($this->urlTo($vars['urls']['styles'].'/delete/'.$styleName))
			->submit('delete', NULL, self::$lang->get('btn_delete'), array('id'=>'delete_'.$styleName, 'class'=>'smallerbox'), true)
			->hidden('name', $styleName, array('id'=>'delete_name_'.$styleName))
			->setFormClass('delete')
				;
		
		return $form;
	}

}
